package com.org.oracle.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.org.oracle.constant.MQConstants;
import com.org.oracle.constant.RedisKeyConstants;
import com.org.oracle.enums.*;
import com.org.oracle.exception.CustomException;
import com.org.oracle.mapper.NoteCollectionMapper;
import com.org.oracle.mapper.NoteCountMapper;
import com.org.oracle.mapper.NoteLikeMapper;
import com.org.oracle.mongodb.dto.CircleQueryBatchDTO;
import com.org.oracle.mongodb.dto.NoteOperateMqDTO;
import com.org.oracle.mongodb.entity.CircleContext;
import com.org.oracle.mongodb.vo.NoteVO;
import com.org.oracle.mongodb.vo.NoteSimpleVO;
import com.org.oracle.mongodb.vo.UserSimpleVO;
import com.org.oracle.mysql.dto.*;
import com.org.oracle.mysql.entity.NoteCollection;
import com.org.oracle.mysql.entity.NoteLike;
import com.org.oracle.mysql.vo.*;
import com.org.oracle.result.PageResult;
import com.org.oracle.service.*;
import com.org.oracle.sub.dto.PageRequestDTO;
import com.org.oracle.util.DateUtils;
import com.org.oracle.util.JsonUtils;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

/**
 * @author: heiye
 * @date: 2024/12/18 下午1:45
 * @version: v1.0.0
 * @description: 笔记
 */
@Slf4j
@Service
public class NoteServiceImpl implements NoteService {

    @Resource
    private MongoTemplate mongoTemplate;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private NoteCountMapper noteCountMapper;

    @Resource
    private NoteLikeMapper noteLikeMapper;

    @Resource(name = "noteExecutor")
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @Resource
    private RocketMQTemplate rocketMQTemplate;

    @Resource
    private NoteCollectionMapper noteCollectionMapper;

    @Resource
    private UserService userService;

    @Resource
    private TopicService topicService;

    @Resource
    private CountService countService;

    @Resource
    private UserRelationService userRelationService;

    @Override
    public CircleContext send_note(String uid, CircleContext circleContext) {
        // 构建时间
        circleContext.setCreateTime(LocalDateTime.now());
        circleContext.setUpdateTime(LocalDateTime.now());

        CircleContext context = mongoTemplate.insert(circleContext);

        // 发送 MQ
        // 构建消息体 DTO
        NoteOperateMqDTO noteOperateMqDTO = NoteOperateMqDTO.builder()
                .noteId(context.getId())
                .creatorId(circleContext.getUid())
                .type(NoteOperateEnum.PUBLISH.getCode())
                .build();

        // 通过冒号连接, 可让 MQ 发送给主题 Topic 时，携带上标签 Tag
        String destination = MQConstants.TOPIC_NOTE_OPERATE + ":" + MQConstants.TAG_NOTE_PUBLISH;

        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(noteOperateMqDTO)).build();

        // 异步发送 MQ 消息，提升接口响应速度
        rocketMQTemplate.asyncSend(destination, message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记发布】MQ 发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记发布】MQ 发送异常: ", throwable);
            }
        });

        return circleContext;
    }

    /***
     *@author suze
     *@date 2024-12-11
     *@time 21:15
     * 批量查询查找找发过的笔记
     **/
    @Override
    public PageResult<NoteSimpleVO> find_note(String uid, PageRequestDTO pageRequestDTO) {
        // 查询该用户发布过多少条笔记
        Query query = Query.query(Criteria.where("uid").is(uid));
        long total = mongoTemplate.count(query, CircleContext.class);

        // 页码
        Integer pageNo = pageRequestDTO.getPage();
        // 条数
        Integer pageSize = pageRequestDTO.getSize();

        // 计算一共多少页
        long totalPage = PageResult.getTotalPage(total, pageSize);

        // 请求的页码超出了总页数
        if (pageNo > totalPage) {
            return new PageResult<>(total, pageNo, pageSize, null);
        }

        // 创建分页对象
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize);

        //在知识圈里面遍历找 对应uid的笔记
        //创建查询对象
        Query query_context = new Query();
        // 添加排序
        query_context.with(Sort.by(Sort.Direction.ASC, "create_time"));
        // 添加筛选字段
        query_context.fields().include("_id", "circle_id", "uid"
                , "topicId", "create_time", "pictures", "title", "word");
        // 添加筛选条件-uid
        query_context.addCriteria(Criteria.where("uid").is(uid));
        // 分页
        query_context.with(pageable);

        // 查询知识圈 用户发过的内容
        List<CircleContext> circleContexts = mongoTemplate.find(query_context, CircleContext.class);

        // 根据笔记查询出笔记作者信息后，构建笔记简要信息反参
        List<NoteSimpleVO> noteSimpleVOS = findNoteSimpleVOSByNotes(circleContexts);

        return new PageResult<>(total, pageNo, pageSize, noteSimpleVOS);
    }

    /**
     * 批量查询笔记
     *
     * @param dto
     * @return
     */
    @Override
    public PageResult<NoteSimpleVO> queryBatch(CircleQueryBatchDTO dto) {
        // 检查参数
        Preconditions.checkArgument(Objects.nonNull(dto), ResponseCodeEnum.PARAM_REQUIRE.getErrorMessage());
        Preconditions.checkArgument(StringUtils.isNotEmpty(dto.getCircleId()), ResponseCodeEnum.PARAM_REQUIRE.getErrorMessage());
        dto.checkParam();

        // 话题ID
        Long topicId = dto.getTopicId();
        // 知识圈ID
        String circleId = dto.getCircleId();

        // 查询该知识圈该话题有多少条笔记
        Query query = Query.query(
                Criteria.where("topicId").is(String.valueOf(topicId))
                        .and("circle_id").is(circleId));
        long total = mongoTemplate.count(query, CircleContext.class);

        // 页码
        Integer pageNo = dto.getPage();
        // 条数
        Integer pageSize = dto.getSize();

        // 计算一共多少页
        long totalPage = PageResult.getTotalPage(total, pageSize);

        // 请求的页码超出了总页数
        if (pageNo > totalPage) {
            return new PageResult<>(total, pageNo, pageSize, null);
        }

        // 创建分页对象
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize);

        // 创建查询对象并添加筛选条件: topic_id
        query = new Query(
                Criteria.where("topicId").is(String.valueOf(topicId))
                        .and("circle_id").is(circleId));
        // 添加筛选字段
        query.fields().include("_id", "uid", "title", "pictures");
        // 添加分页操作
        query.with(pageable);
        // 添加排序
        query.with(Sort.by(Sort.Direction.ASC, "create_time"));
        // 查询
        List<CircleContext> circleContexts = mongoTemplate.find(query, CircleContext.class);

        // 根据笔记查询出笔记作者信息后，构建笔记简要信息反参
        List<NoteSimpleVO> noteSimpleVOS = findNoteSimpleVOSByNotes(circleContexts);

        // 返回数据
        return new PageResult<>(total, pageNo, pageSize, noteSimpleVOS);
    }

    /**
     * 查询笔记详情
     *
     * @param
     * @return
     */
    @Override
    @SneakyThrows
    public NoteVO queryOne(String uid, String noteId) {
        // 创建查询对象
        Query query = new Query();

        // 添加筛选字段
        query.fields().include("_id", "uid", "topicId", "pictures", "title", "word");
        //添加筛选条件
        query.addCriteria(Criteria.where("_id").is(noteId));

        // 查询
        CircleContext circleContext = mongoTemplate.findOne(query, CircleContext.class);
        // 判空
        if (Objects.isNull(circleContext)) {
            throw new CustomException(ResponseCodeEnum.NOTE_NOT_FIND);
        }

        // 笔记作者 ID
        String creatorId = circleContext.getUid();
        // 话题 AI 的 ID
        String topicId = circleContext.getTopicId();

        // 获得话题 AI 基本信息
        CompletableFuture<TopicVO> topicResultFuture = CompletableFuture
                .supplyAsync(() -> topicService.findTopicById(topicId), threadPoolTaskExecutor);
        // 获得笔记作者基本信息
        CompletableFuture<UserSimpleVO> userResultFuture = CompletableFuture
                .supplyAsync(() -> userService.findUserSimpleById(creatorId), threadPoolTaskExecutor);
        // 获得笔记计数信息
        CompletableFuture<NoteCountVO> countResultFuture = CompletableFuture
                .supplyAsync(() -> countService.findNoteCountByNoteId(noteId), threadPoolTaskExecutor);
        // 获得用户是否对笔记作者进行关注
        CompletableFuture<Boolean> userIsFollowingResultFuture = CompletableFuture
                .supplyAsync(() -> userRelationService.findUserRelation(uid, creatorId), threadPoolTaskExecutor);
        // 获得用户对笔记是否点赞收藏情况
        CompletableFuture<Boolean> userIsLikeNoteResultFuture = CompletableFuture
                .supplyAsync(() -> userIsLikedNote(uid, noteId), threadPoolTaskExecutor);
        CompletableFuture<Boolean> userIsCollectNoteResultFuture = CompletableFuture
                .supplyAsync(() -> userIsCollectedNote(uid, noteId), threadPoolTaskExecutor);
        // 异步调用
        CompletableFuture<NoteVO> resultFuture = CompletableFuture
                .allOf(
                        topicResultFuture,
                        userResultFuture,
                        countResultFuture,
                        userIsFollowingResultFuture,
                        userIsLikeNoteResultFuture,
                        userIsCollectNoteResultFuture)
                .thenApply(s -> {
                    // 获取异步结果
                    TopicVO topicVO = topicResultFuture.join();
                    UserSimpleVO userSimpleVO = userResultFuture.join();
                    NoteCountVO noteCountVO = countResultFuture.join();
                    Boolean isFollowingCreator = userIsFollowingResultFuture.join();
                    boolean isLike = userIsLikeNoteResultFuture.join();
                    boolean isCollect = userIsCollectNoteResultFuture.join();

                    // 构建反参
                    return NoteVO.builder()
                            .id(noteId)
                            .creatorId(creatorId)
                            .topicId(topicId)
                            .topicName(topicVO.getName())
                            .creatorName(userSimpleVO.getName())
                            .creatorAvatar(userSimpleVO.getAvatar())
                            .title(circleContext.getTitle())
                            .pictures(circleContext.getPictures())
                            .word(circleContext.getWord())
                            .likeTotal(noteCountVO.getLikeTotal())
                            .collectTotal(noteCountVO.getCollectTotal())
                            .commentTotal(noteCountVO.getCommentTotal())
                            .isLike(isLike)
                            .isCollect(isCollect)
                            .isFollowingCreator(isFollowingCreator)
                            .build();
                });
        return resultFuture.get();
    }

    /***
     *@author suze
     *@date 2024-12-11
     *@time 21:15
     * 存草稿
     **/
    @Override
    public CircleContext save_note(String uid, CircleContext circleContext) {
        mongoTemplate.insert(circleContext, "draft");
        return null;
    }

    /***
     *@author suze
     *@date 2024-12-11
     *@time 21:15
     * 找草稿箱的笔记
     *
     **/
    @Override
    public PageResult<NoteSimpleVO> get_save_note(String uid, PageRequestDTO pageRequestDTO) {

        // 查询用户保存过多少条笔记
        Query query = Query.query(Criteria.where("uid").is(uid));
        long total = mongoTemplate.count(query, CircleContext.class, "draft");

        // 页码
        Integer pageNo = pageRequestDTO.getPage();
        // 条数
        Integer pageSize = pageRequestDTO.getSize();

        // 计算一共多少页
        long totalPage = PageResult.getTotalPage(total, pageSize);

        // 请求的页码超出了总页数
        if (pageNo > totalPage) {
            return new PageResult<>(total, pageNo, pageSize, null);
        }

        // 创建分页对象
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize);

        // 创建查询对象
        query = Query.query(Criteria.where("uid").is(uid));
        // 添加排序
        query.with(Sort.by(Sort.Direction.DESC, "create_time"));
        // 添加筛选字段
        query.fields().include("_id", "uid", "pictures", "title");
        // 分页
        query.with(pageable);
        // 查询
        List<CircleContext> circleContexts = mongoTemplate.find(query, CircleContext.class, "draft");

        // 根据笔记查询出笔记作者信息后，构建笔记简要信息反参
        List<NoteSimpleVO> noteSimpleVOS = findNoteSimpleVOSByNotes_forDraft(circleContexts);

        // 返回数据
        return new PageResult<>(total, pageNo, pageSize, noteSimpleVOS);
    }

    /**
     * 根据笔记查询出笔记作者信息以及笔记点赞数后，再返回笔记简要信息
     *
     * @param notes
     */
    @SneakyThrows
    private List<NoteSimpleVO> findNoteSimpleVOSByNotes(List<CircleContext> notes) {

        // 并发查询优化，获取笔记作者信息和点赞数信息（批量查询）
        CompletableFuture<List<UserSimpleVO>> userResultFuture = CompletableFuture
                .supplyAsync(() -> {
                    // 获取所有笔记作者 ID
                    List<String> userIds = notes.stream().map(CircleContext::getUid).distinct().toList();
                    // 调用用户服务
                    return userService.findUserSimpleByIds(userIds);
                }, threadPoolTaskExecutor);
        CompletableFuture<List<NoteLikeTotalVO>> countResultFuture = CompletableFuture
                .supplyAsync(() -> {
                    // 获取所有笔记 ID
                    List<String> noteIds = notes.stream().map(CircleContext::getId).toList();
                    // 调用计数服务
                    return countService.findNoteLikeTotalByNoteIds(noteIds);
                }, threadPoolTaskExecutor);

        CompletableFuture<List<NoteSimpleVO>> resultFuture = CompletableFuture
                .allOf(userResultFuture, countResultFuture)
                .thenApply(s -> {
                    // 获取 Future 返回的结果
                    List<UserSimpleVO> userSimpleByIds = userResultFuture.join();
                    List<NoteLikeTotalVO> noteLikeTotalByNoteIds = countResultFuture.join();

                    // 使用 Map 存储笔记作者信息
                    Map<String, UserSimpleVO> userSimpleMap = userSimpleByIds.stream().collect(
                            Collectors.toMap(
                                    UserSimpleVO::getUid,
                                    value -> value
                            )
                    );

                    // 使用 Map 存储笔记点赞数信息
                    Map<String, Long> noteLikeTotalMap = noteLikeTotalByNoteIds.stream().collect(
                            Collectors.toMap(
                                    NoteLikeTotalVO::getNoteId,
                                    NoteLikeTotalVO::getLikeTotal
                            ));

                    // 构建结果集
                    List<NoteSimpleVO> noteSimpleVOS = Lists.newArrayList();

                    for (CircleContext note : notes) {
                        // 笔记作者 ID
                        String uid = note.getUid();
                        // 笔记ID
                        String noteId = note.getId();

                        // 获取对应的笔记作者基本信息
                        UserSimpleVO userSimpleVO = userSimpleMap.get(uid);
                        // 获取对应的笔记的获得点赞的信息
                        Long likeTotal = noteLikeTotalMap.get(noteId);

                        // 判断该笔记作者是否出现不存在的情况
                        if (Objects.isNull(userSimpleVO)) {
                            continue;
                        }

                        // 笔记图片数组
                        List<String> pictures = note.getPictures();

                        // 笔记封面
                        String picture = null;
                        if (CollUtil.isNotEmpty(pictures)) {
                            picture = pictures.get(0);
                        }

                        noteSimpleVOS.add(NoteSimpleVO.builder()
                                .id(note.getId())
                                .creatorId(userSimpleVO.getUid())
                                .creatorAvatar(userSimpleVO.getAvatar())
                                .creatorName(userSimpleVO.getName())
                                .title(note.getTitle())
                                .likeTotal(likeTotal)
                                .picture(picture)
                                .build());
                    }
                    return noteSimpleVOS;
                });
        return resultFuture.get();
    }

    /**
     * 根据笔记列表获取简化的笔记信息列表，用于草稿状态的笔记
     * 此方法主要对笔记进行简化处理，包括提取笔记作者信息和笔记封面图片
     *
     * @param notes 笔记列表，应包含草稿状态的笔记
     * @return 返回一个简化的笔记信息列表，如果输入列表为空，则返回空列表
     */
    private List<NoteSimpleVO> findNoteSimpleVOSByNotes_forDraft(List<CircleContext> notes) {
        if (notes.isEmpty()) {
            return Collections.emptyList();
        }
        // 获取笔记作者 ID
        String uid = notes.get(0).getUid();

        // 调用用户服务获取用户信息
        UserSimpleVO userSimpleVO = userService.findUserSimpleById(uid);

        // 构建结果集
        List<NoteSimpleVO> noteSimpleVOS = Lists.newArrayList();

        for (CircleContext note : notes) {
            // 笔记图片数组
            List<String> pictures = note.getPictures();

            // 笔记封面
            String picture = null;
            if (CollUtil.isNotEmpty(pictures)) {
                picture = pictures.get(0);
            }
            noteSimpleVOS.add(NoteSimpleVO.builder()
                    .id(note.getId())
                    .creatorId(userSimpleVO.getUid())
                    .creatorAvatar(userSimpleVO.getAvatar())
                    .creatorName(userSimpleVO.getName())
                    .title(note.getTitle())
                    .picture(picture)
                    .likeTotal(0L) //虽然没有 但也要有这个字段不然前端会报错
                    .build());
        }
        return noteSimpleVOS;
    }


    /***
     *@author suze
     *@date 2024-12-11
     *@time 21:15
     * 草稿箱的笔记 点进详情
     *
     **/
    @Override
    public CircleContext get_save_note_one(String uid, String noteId) {
        //查询草稿箱里详情的笔记
        Query query = Query.query(Criteria.where("_id").is(noteId));
        CircleContext circleContext = mongoTemplate.findOne(query, CircleContext.class, "draft");
        return circleContext;
    }


    /**
     * 点赞笔记
     *
     * @param uid
     * @param likeNoteDTO
     * @return
     */
    @Override
    public void likeNote(String uid, LikeNoteDTO likeNoteDTO) {
        // 笔记ID
        String noteId = likeNoteDTO.getNoteId();

        // 1. 校验被点赞的笔记是否存在，若存在，则获取发布者用户 ID
        String creatorId = checkNoteIsExist(noteId);

        // 2. 判断目标笔记，是否已经赞过
        if (userIsLikedNote(uid, noteId)) {
            // 抛出用户点赞过该笔记异常
            throw new CustomException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
        }

        // 3.更新用户 ZSET 点赞列表
        String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(uid);
        LocalDateTime now = LocalDateTime.now();
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        // lua 脚本路径
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/note_like_check_and_update_zset.lua")));
        // 返回值类型
        script.setResultType(Long.class);
        // 执行脚本
        Long result = redisTemplate.execute(script, Collections.singletonList(userNoteLikeZSetKey), noteId, DateUtils.localDateTime2Timestamp(now));

        // 若 ZSet 列表不存在，需要重新初始化
        if (Objects.equals(result, NoteLikeLuaResultEnum.NOT_EXIST.getCode())) {

            // 查询最近点赞的100条笔记
            List<NoteLike> noteLikes = selectLikedNotes(uid);

            if (CollUtil.isNotEmpty(noteLikes)) {
                // 保底 1 天 + 随机秒数
                long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
                // 构建 Lua 参数
                Object[] lugArgs = buildNoteLikeZSetLuaArgs(noteLikes, expireSeconds);

                DefaultRedisScript<Long> script1 = new DefaultRedisScript<>();
                // Lua 脚本路径
                script1.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_like_zset_and_expire.lua")));
                // 返回值类型
                script1.setResultType(Long.class);
                // 初始化 ZSet 列表
                redisTemplate.execute(script1, Collections.singletonList(userNoteLikeZSetKey), lugArgs);
                // 再次调用 note_like_check_and_update_zset.lua 脚本，将点赞的笔记添加到 zset 中
                redisTemplate.execute(script, Collections.singletonList(userNoteLikeZSetKey), noteId, DateUtils.localDateTime2Timestamp(now));
            }
        }

        // 4.将点赞数据落库
        // 构建消息体 DTO
        LikeUnlikeNoteMqDTO likeUnlikeNoteMqDTO = LikeUnlikeNoteMqDTO.builder()
                .uid(uid)
                .noteId(noteId)
                // 点赞笔记
                .type(LikeUnlikeNoteTypeEnum.LIKE.getCode())
                .createTime(now)
                // 笔记发布者 ID
                .noteCreatorId(creatorId)
                .build();

        // 构建消息对象，并将 DTO 转成 Json 字符串设置到消息体中
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(likeUnlikeNoteMqDTO)).build();

        // 通过冒号连接，可让 MQ 发送给主题 Topic 时，携带上标签 Tag
        String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_LIKE;

        // 异步发送 MQ 消息，提升接口响应速度
        rocketMQTemplate.asyncSendOrderly(destination, message, uid, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记点赞】MQ 发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记点赞】MQ 发送异常: ", throwable);
            }
        });
    }

    /**
     * 取消点赞笔记
     *
     * @param uid
     * @param unLikeNoteDTO
     */
    @Override
    public void unlikeNote(String uid, UnLikeNoteDTO unLikeNoteDTO) {
        // 笔记 ID
        String noteId = unLikeNoteDTO.getNoteId();

        // 1.校验笔记是否存在
        String creatorId = checkNoteIsExist(noteId);

        // 2.校验笔记是否点赞过
        // 布隆过滤器 key
        String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(uid);

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        // Lua 脚本路径
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_unlike_check.lua")));
        // 返回值类型
        script.setResultType(Long.class);

        // 执行 Lua 脚本，拿到返回结果
        Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId);

        NoteUnlikeLuaResultEnum noteUnlikeLuaResultEnum = NoteUnlikeLuaResultEnum.valueOf(result);

        switch (noteUnlikeLuaResultEnum) {
            // 布隆过滤器不存在
            case NOT_EXIST -> {
                // 异步初始化布隆过滤器
                threadPoolTaskExecutor.execute(() -> {
                    // 保底 1 天 + 随机秒数
                    long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
                    batchAddNoteLike2BloomAndExpire(uid, expireSeconds, bloomUserNoteLikeListKey);
                });

                // 从数据库中校验笔记是否被点赞
                long count = selectNoteIsLiked(uid, noteId);

                // 未点赞，无法取消点赞操作，抛出业务异常
                if (count == 0) {
                    throw new CustomException(ResponseCodeEnum.NOTE_NOT_LIKED);
                }
            }
            // 布隆过滤器校验目标笔记未被点赞（判断绝对正确）
            case NOTE_NOT_LIKED -> throw new CustomException(ResponseCodeEnum.NOTE_NOT_LIKED);
        }

        // 3.能走到这里，说明布隆过滤器判断已点赞，直接删除 ZSET 中已点赞的笔记 ID
        // 用户点赞列表 ZSet Key
        String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(uid);

        redisTemplate.opsForZSet().remove(userNoteLikeZSetKey, noteId);

        // 4.取消点赞数据落库
        // 构建消息体 DTO
        LikeUnlikeNoteMqDTO likeUnlikeNoteMqDTO = LikeUnlikeNoteMqDTO.builder()
                .uid(uid)
                .noteId(noteId)
                // 取消点赞笔记
                .type(LikeUnlikeNoteTypeEnum.UNLIKE.getCode())
                .createTime(LocalDateTime.now())
                .noteCreatorId(creatorId)
                .build();

        // 构建消息对象，并将 DTO 转成 Json 字符串设置到消息体中
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(likeUnlikeNoteMqDTO)).build();
        // 通过冒号连接，可让 MQ 发送给主题 Topic 时，携带上标签 Tag
        String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_UNLIKE;

        // 异步发送 MQ 消息，提升接口响应速度
        rocketMQTemplate.asyncSendOrderly(destination, message, uid, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记取消点赞】MQ 发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记取消点赞】MQ 发送异常: ", throwable);
            }
        });
    }

    /**
     * 收藏笔记
     *
     * @param uid
     * @param collectNoteDTO
     */
    @Override
    public void collectNote(String uid, CollectNoteDTO collectNoteDTO) {
        // 获取笔记ID
        String noteId = collectNoteDTO.getNoteId();

        // 1. 校验笔记是否存在
        String creatorId = checkNoteIsExist(noteId);

        // 2. 判断目标笔记，是否已经收藏过
        if (userIsCollectedNote(uid, noteId)) {
            // 抛出用户收藏过该笔记异常
            throw new CustomException(ResponseCodeEnum.NOTE_ALREADY_COLLECTED);
        }

        // 3. 更新目标用户的收藏 ZSET 列表
        // 用户收藏列表 ZSet Key
        String userNoteCollectZSetKey = RedisKeyConstants.buildUserNoteCollectZSetKey(uid);

        // 获取当前时间
        LocalDateTime now = LocalDateTime.now();

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        // Lua 脚本路径
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/note_collect_check_and_update_zset.lua")));
        // 返回值类型
        script.setResultType(Long.class);

        // 执行 Lua 脚本，拿到返回结果
        Long result = redisTemplate.execute(script, Collections.singletonList(userNoteCollectZSetKey), noteId, DateUtils.localDateTime2Timestamp(now));

        // 若 ZSet 列表不存在，需要重新初始化
        if (Objects.equals(result, NoteCollectLuaResultEnum.NOT_EXIST.getCode())) {
            // 查询当前用户最新收藏的 300 篇笔记
            List<NoteCollection> noteCollections = selectCollectedNotes(uid);

            if (CollUtil.isNotEmpty(noteCollections)) {
                // 保底 1 天 + 随机秒数
                long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
                // 构建 Lua 参数
                Object[] luaArgs = buildNoteCollectionZSetLuaArgs(noteCollections, expireSeconds);

                DefaultRedisScript<Long> script1 = new DefaultRedisScript<>();
                // Lua 脚本路径
                script1.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_collect_zset_and_expire.lua")));
                // 返回值类型
                script1.setResultType(Long.class);

                // 执行 Lua 脚本
                redisTemplate.execute(script1, Collections.singletonList(userNoteCollectZSetKey), luaArgs);
                // 再次调用 note_collect_check_and_update_zset.lua 脚本，将当前收藏的笔记添加到 zset 中
                redisTemplate.execute(script, Collections.singletonList(userNoteCollectZSetKey), noteId, DateUtils.localDateTime2Timestamp(now));
            }

        }
        // 4. 发送 MQ, 将收藏数据落库
        // 构建消息体 DTO
        CollectUnCollectNoteMqDTO collectUnCollectNoteMqDTO = CollectUnCollectNoteMqDTO.builder()
                .uid(uid)
                .noteId(noteId)
                // 收藏笔记
                .type(CollectUnCollectNoteTypeEnum.COLLECT.getCode())
                .createTime(now)
                // 发布笔记作者ID
                .noteCreatorId(creatorId)
                .build();

        // 构建消息对象，并将 DTO 转成 Json 字符串设置到消息体中
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(collectUnCollectNoteMqDTO)).build();

        // 通过冒号连接, 可让 MQ 发送给主题 Topic 时，携带上标签 Tag
        String destination = MQConstants.TOPIC_COLLECT_OR_UN_COLLECT + ":" + MQConstants.TAG_COLLECT;
        // 异步发送顺序 MQ 消息，提升接口响应速度
        rocketMQTemplate.asyncSendOrderly(destination, message, uid, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记收藏】MQ 发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记收藏】MQ 发送异常: ", throwable);
            }
        });
    }

    /**
     * 取消收藏笔记
     *
     * @param uid
     * @param unCollectNoteDTO
     */
    @Override
    public void unCollectNote(String uid, UnCollectNoteDTO unCollectNoteDTO) {
        // 笔记ID
        String noteId = unCollectNoteDTO.getNoteId();

        // 1.校验笔记是否真实存在
        String creatorId = checkNoteIsExist(noteId);

        // 2.校验笔记是否被收藏过
        // 布隆过滤器 Key
        String bloomUserNoteCollectListKey = RedisKeyConstants.buildBloomUserNoteCollectListKey(uid);

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        // Lua 脚本路径
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_uncollect_check.lua")));
        // 返回值类型
        script.setResultType(Long.class);

        // 执行 Lua 脚本，拿到返回结果
        Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteCollectListKey), noteId);

        NoteUnCollectLuaResultEnum noteUnCollectLuaResultEnum = NoteUnCollectLuaResultEnum.valueOf(result);

        switch (noteUnCollectLuaResultEnum) {
            // 布隆过滤器不存在
            case NOT_EXIST -> {
                // 异步初始化布隆过滤器
                threadPoolTaskExecutor.execute(() -> {
                    // 保底 1 天 + 随机秒数
                    long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
                    batchAddNoteCollect2BloomAndExpire(uid, expireSeconds, bloomUserNoteCollectListKey);
                });

                // 从数据库中校验笔记是否被收藏
                Long count = selectNoteIsCollected(uid, noteId);

                // 未收藏，无法取消收藏操作，抛出业务异常
                if (count == 0) {
                    throw new CustomException(ResponseCodeEnum.NOTE_NOT_COLLECTED);
                }
            }
            // 布隆过滤器校验目标笔记未被收藏（判断绝对正确）
            case NOTE_NOT_COLLECTED -> throw new CustomException(ResponseCodeEnum.NOTE_NOT_COLLECTED);
        }
        // 3.删除 ZSET 中已收藏的笔记 ID
        // 能走到这里，说明布隆过滤器判断已收藏，直接删除 ZSET 中已收藏的笔记 ID
        // 用户收藏列表 ZSet Key
        String userNoteCollectZSetKey = RedisKeyConstants.buildUserNoteCollectZSetKey(uid);
        redisTemplate.opsForZSet().remove(userNoteCollectZSetKey, noteId);

        // 4.发送 MQ, 数据更新落库
        // 构建消息体 DTO
        CollectUnCollectNoteMqDTO collectUnCollectNoteMqDTO = CollectUnCollectNoteMqDTO.builder()
                .uid(uid)
                .noteId(noteId)
                // 取消收藏笔记
                .type(CollectUnCollectNoteTypeEnum.UN_COLLECT.getCode())
                .createTime(LocalDateTime.now())
                // 笔记发布者 ID
                .noteCreatorId(creatorId)
                .build();

        // 构建消息对象，并将 DTO 转成 Json 字符串设置到消息体中
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(collectUnCollectNoteMqDTO)).build();

        // 通过冒号连接, 可让 MQ 发送给主题 Topic 时，携带上标签 Tag
        String destination = MQConstants.TOPIC_COLLECT_OR_UN_COLLECT + ":" + MQConstants.TAG_UN_COLLECT;

        // 异步发送顺序 MQ 消息，提升接口响应速度
        rocketMQTemplate.asyncSendOrderly(destination, message, uid, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记取消收藏】MQ 发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记取消收藏】MQ 发送异常: ", throwable);
            }
        });
    }

    /**
     * 查询用户收藏过的笔记
     *
     * @param uid
     * @param pageRequestDTO
     * @return
     */
    @Override
    public PageResult<NoteSimpleVO> findUserCollectedNote(String uid, PageRequestDTO pageRequestDTO) {
        // 校验参数
        pageRequestDTO.checkParam();

        // 页码
        Integer pageNo = pageRequestDTO.getPage();
        // 每页展示 limit 条数据
        Integer limit = pageRequestDTO.getSize();

        // 先从 Redis 中查询
        String userNoteCollectZSetKey = RedisKeyConstants.buildUserNoteCollectZSetKey(uid);

        // 查询目标用户关注列表 ZSet 的总大小
        long total = redisTemplate.opsForZSet().zCard(userNoteCollectZSetKey);

        // 返参
        List<NoteSimpleVO> noteSimpleVOS = null;

        // 缓存中有数据
        if (total > 0) {
            // 计算一共 Zset 列表可以分为多少页
            long totalPage = PageResult.getTotalPage(total, limit);

            // 请求的页码超出了 ZSet 列表总页数 说明需要查找数据库
            if (pageNo > totalPage) {
                // 查询用户收藏了多少条笔记
                total = findUserCollectedNoteTotal(uid);

                // 计算一共多少页
                totalPage = PageResult.getTotalPage(total, limit);

                // 请求的页码超出了总页数
                if (pageNo > totalPage) {
                    return new PageResult<>(total, pageNo, limit, null);
                }

                // 分页查询用户收藏的笔记
                List<NoteCollection> noteCollections = findUserAllNoteCollects(uid, pageNo, limit);

                // 若记录不为空
                if (CollUtil.isNotEmpty(noteCollections)) {
                    // 构建用户收藏的笔记 ID 列表
                    List<String> userNoteCollectIds = noteCollections.stream().map(NoteCollection::getNoteId).toList();

                    // 根据笔记 ID 批量查询笔记
                    List<CircleContext> userNoteCollects = findNoteByIds(userNoteCollectIds);
                    noteSimpleVOS = findNoteSimpleVOSByNotes(userNoteCollects);
                }

                return new PageResult<>(total, pageNo, limit, noteSimpleVOS);
            }

            // 每页 limit 个元素，计算偏移量
            long offset = PageResult.getOffset(pageNo, limit);

            // 准备从 Redis 中查询 ZSet 分页数据
            // 使用 ZREVRANGEBYSCORE 命令按 score 降序获取元素，同时使用 LIMIT 子句实现分页
            // 注意：这里使用了 Double.POSITIVE_INFINITY 和 Double.NEGATIVE_INFINITY 作为分数范围
            // 因为收藏列表最多有 300 个元素，这样可以确保获取到所有的元素
            Set<Object> userNoteCollectIdsSet = redisTemplate.opsForZSet().reverseRangeByScore(userNoteCollectZSetKey, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, offset, limit);

            if (CollUtil.isNotEmpty(userNoteCollectIdsSet)) {
                // 提取所有笔记 ID 到集合中
                List<String> userNoteCollectIds = userNoteCollectIdsSet.stream().map(Object::toString).toList();

                // 根据笔记 ID 批量查询笔记
                List<CircleContext> userNoteCollects = findNoteByIds(userNoteCollectIds);
                noteSimpleVOS = findNoteSimpleVOSByNotes(userNoteCollects);
            }
        } else {
            // 若 Redis 中没有数据，则从数据库查询
            // 查询用户收藏了多少条笔记
            total = findUserCollectedNoteTotal(uid);

            // 计算一共多少页
            long totalPage = PageResult.getTotalPage(total, limit);

            // 请求的页码超出了总页数
            if (pageNo > totalPage) {
                return new PageResult<>(total, pageNo, limit, null);
            }

            // 分页查询用户收藏的笔记
            List<NoteCollection> noteCollections = findUserAllNoteCollects(uid, pageNo, limit);

            // 若记录不为空
            if (CollUtil.isNotEmpty(noteCollections)) {
                // 构建用户收藏的笔记 ID 列表
                List<String> userNoteCollectIds = noteCollections.stream().map(NoteCollection::getNoteId).toList();

                // 根据笔记 ID 批量查询笔记
                List<CircleContext> userNoteCollects = findNoteByIds(userNoteCollectIds);
                noteSimpleVOS = findNoteSimpleVOSByNotes(userNoteCollects);

                // 异步将用户收藏笔记同步到 Redis
                threadPoolTaskExecutor.submit(() -> asynInitUserNoteCollectionsZSet(uid, userNoteCollectZSetKey));
            }
        }

        return new PageResult<>(total, pageNo, limit, noteSimpleVOS);
    }

    /**
     * 获取笔记的点赞信息
     *
     * @param uid    用户ID，用于判断该用户是否点赞
     * @param noteId 笔记ID，用于获取点赞信息
     * @return NoteLikeResponseVO 包含点赞信息的响应对象
     */
    public NoteLikeResponseVO getNoteLikes(String uid, String noteId) {
        // 1. 判断当前用户是否已点赞
        boolean userLikedNote = userIsLikedNote(uid, noteId);
        // 2.查询Redis中该笔记的点赞总数
        String countNoteRedisKey = RedisKeyConstants.buildCountNoteKey(noteId);
        // 从Redis中获取笔记的点赞总数
        Long likeCount = (Long) redisTemplate.opsForHash().get(countNoteRedisKey, RedisKeyConstants.FIELD_LIKE_TOTAL);
        // 3.若Redis中获取的为空 则查询数据库-查询该笔记的点赞总数
        if (likeCount == null) {
            likeCount = Long.valueOf(noteCountMapper.selectLikeTotalByNoteId(noteId));
            // 缓存到Redis中
            redisTemplate.opsForHash().increment(countNoteRedisKey, RedisKeyConstants.FIELD_LIKE_TOTAL, likeCount);
        }
        // 4. 构建响应DTO
        NoteLikeResponseVO responseVO = NoteLikeResponseVO.builder()
                .noteId(noteId)
                .likeCount(likeCount)
                .isLike(userLikedNote)
                .build();
        return responseVO;
    }

    /**
     * 获取笔记的收藏信息
     *
     * @param uid    用户ID，用于判断该用户是否收藏
     * @param noteId 笔记ID，用于获取收藏信息
     * @return NoteCollectionResponseVO 包含收藏信息的响应对象
     */
    public NoteCollectionResponseVO getNoteCollections(String uid, String noteId) {
        // 1. 判断当前用户是否已收藏该笔记
        boolean userCollectedNote = userIsCollectedNote(uid, noteId);
        // 2. 查询Redis中该笔记的收藏总数
        String countNoteRedisKey = RedisKeyConstants.buildCountNoteKey(noteId);
        // 从Redis中获取笔记的收藏总数
        Long collectCount = (Long) redisTemplate.opsForHash().get(countNoteRedisKey, RedisKeyConstants.FIELD_COLLECT_TOTAL);
        // 3. 若Redis中获取的为空 则查询数据库-查询该笔记的收藏总数
        if (collectCount == null) {
            collectCount = Long.valueOf(noteCountMapper.selectCollectTotalByNoteId(noteId));
            // 缓存到Redis中
            redisTemplate.opsForHash().put(countNoteRedisKey, RedisKeyConstants.FIELD_COLLECT_TOTAL, collectCount);
        }
        // 4. 构建响应DTO
        NoteCollectionResponseVO responseVO = NoteCollectionResponseVO.builder()
                .noteId(noteId)
                .collectCount(collectCount)
                .isCollected(userCollectedNote)
                .build();

        return responseVO;
    }

    /**
     * 检查用户是否喜欢了特定的笔记（查了布隆+ZSET+MySQL）
     *
     * @param uid    用户ID，用于标识用户
     * @param noteId 笔记ID，用于标识特定的笔记
     * @return 返回true如果用户喜欢了该笔记，否则返回false
     */
    private boolean userIsLikedNote(String uid, String noteId) {
        // 布隆过滤器 key
        String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(uid);

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        // Lua 脚本路径
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_like_check.lua")));
        // 返回值类型
        script.setResultType(Long.class);

        // 执行 Lua 脚本，检查是否点过赞（在布隆过滤器层面）：拿到返回结果
        Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId);

        NoteLikeLuaResultEnum noteLikeLuaResultEnum = NoteLikeLuaResultEnum.valueOf(result);

        // 用户点赞列表 ZSet Key
        String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(uid);

        switch (noteLikeLuaResultEnum) {
            // 目标笔记不存在于Redis的布隆过滤器中
            // 则有可能分为已点赞但不在Redis or 未点赞
            case NOT_EXIST -> {
                // 从数据库中校验笔记是否被点赞，并异步初始化布隆过滤器，设置过期时间
                long count = selectNoteIsLiked(uid, noteId);
                // 保底 1 天 + 随机秒数
                long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);

                // 目标笔记已被点赞
                if (count > 0) {
                    // 异步初始化布隆过滤器
                    threadPoolTaskExecutor.submit(() -> batchAddNoteLike2BloomAndExpire(uid, expireSeconds, bloomUserNoteLikeListKey));
                    return true;
                }

                // 若目标笔记未被点赞，查询当前用户是否有点赞其他笔记，有则同步初始化布隆过滤器
                batchAddNoteLike2BloomAndExpire(uid, expireSeconds, bloomUserNoteLikeListKey);
                return false;
            }
            // 目标笔记已经被点赞 (可能存在误判，需要进一步确认)
            case NOTE_LIKED -> {
                // 校验 Zset 列表中是否包含被点赞的笔记ID
                Double score = redisTemplate.opsForZSet().score(userNoteLikeZSetKey, noteId);
                // 判空
                if (Objects.nonNull(score)) {
                    return true;
                }
                // 若 Score 为空，则表示 ZSet 点赞列表中不存在，查询数据库校验
                long count = selectNoteIsLiked(uid, noteId);
                // 判断是否存在数据
                if (count > 0) {
                    // 数据库里面有点赞记录，而 Redis 中 ZSet 不存在，需要异步初始化 Zet
                    asynInitUserNoteLikesZSet(uid, userNoteLikeZSetKey);
                    return true;
                }
                // 执行到这一步说明用户没有点赞该笔记，但是布隆过滤器里面有数据，需要同步初始化 ZSet
                return false;
            }
        }
        return false;
    }

    /**
     * 检查用户是否收藏了指定的笔记
     *
     * @param uid    用户ID
     * @param noteId 笔记ID
     * @return 如果用户收藏了指定的笔记，则返回true；否则返回false
     */
    public boolean userIsCollectedNote(String uid, String noteId) {
        // 构建用户 bloom 过滤器的 KEY
        String bloomUserNoteCollectListKey = RedisKeyConstants.buildBloomUserNoteCollectListKey(uid);

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        // Lua 脚本路径
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_collect_check.lua")));
        // 定义返回类型
        script.setResultType(Long.class);

        // 执行 lua 脚本
        Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteCollectListKey), noteId);

        NoteCollectLuaResultEnum noteCollectLuaResultEnum = NoteCollectLuaResultEnum.valueOf(result);

        // 用户收藏列表 ZSet Key
        String userNoteCollectZSetKey = RedisKeyConstants.buildUserNoteCollectZSetKey(uid);

        switch (noteCollectLuaResultEnum) {
            // Redis 中布隆过滤器不存在
            case NOT_EXIST -> {
                // 从数据库中校验笔记是否被收藏，并异步初始化布隆过滤器，设置过期时间
                long count = selectNoteIsCollected(uid, noteId);

                // 保底 1 天 + 随机秒数
                long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);

                // 目标笔记已经被收藏
                if (count > 0) {
                    // 异步初始化布隆过滤器
                    threadPoolTaskExecutor.submit(() -> batchAddNoteCollect2BloomAndExpire(uid, expireSeconds, bloomUserNoteCollectListKey));
                    return true;
                }

                // 若目标笔记未被收藏，查询当前用户是否有收藏其他笔记，有则同步初始化布隆过滤器
                batchAddNoteCollect2BloomAndExpire(uid, expireSeconds, bloomUserNoteCollectListKey);

                // Lua 脚本路径
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_add_note_collect_and_expire.lua")));
                // 返回值类型
                script.setResultType(Long.class);
                // 执行 Lua 脚本
                redisTemplate.execute(script, Collections.singletonList(bloomUserNoteCollectListKey), noteId, expireSeconds);
                return false;
            }
            // 目标笔记已被收藏（可能存在误判，需要进一步判断）
            case NOTE_COLLECTED -> {
                // 校验 ZSet 列表是否包含被收藏的笔记ID
                Double score = redisTemplate.opsForZSet().score(userNoteCollectZSetKey, noteId);

                // 判空
                if (Objects.nonNull(score)) {
                    return true;
                }

                // 若 score 为空，则表示 ZSet 收藏列表不存在，查询数据库进行校验
                Long count = selectNoteIsCollected(uid, noteId);

                if (count > 0) {
                    // 数据库里面有收藏记录，而 Redis 中 ZSet 已过期被删除的话，需要重新异步初始化 ZSet
                    asynInitUserNoteCollectionsZSet(uid, userNoteCollectZSetKey);
                    return true;
                }

                return false;
            }
        }
        return false;
    }


    /**
     * 根据笔记 ID 批量查询笔记
     *
     * @param ids
     * @return
     */
    private List<CircleContext> findNoteByIds(List<String> ids) {
        // 批量查询对应笔记信息
        Query query = new Query(Criteria.where("_id").in(ids));
        // 筛选字段
        query.fields().include("_id", "uid", "title", "pictures");
        return mongoTemplate.find(query, CircleContext.class);
    }

    /**
     * 分页查询用户收藏的笔记
     *
     * @param uid
     * @param pageNo
     * @param limit
     * @return
     */
    private List<NoteCollection> findUserAllNoteCollects(String uid, Integer pageNo, Integer limit) {
        // 在数据库中查找用户收藏过的笔记
        LambdaQueryWrapper<NoteCollection> lambdaQueryWrapper = Wrappers.<NoteCollection>lambdaQuery()
                .select(NoteCollection::getNoteId)
                .eq(NoteCollection::getUid, uid)
                .eq(NoteCollection::getStatus, CollectUnCollectNoteTypeEnum.COLLECT.getCode())
                .orderByDesc(NoteCollection::getCreateTime);
        // 分页
        Page<NoteCollection> page = new Page<>(pageNo, limit);
        return noteCollectionMapper.selectList(page, lambdaQueryWrapper);
    }

    /**
     * 查询用户收藏了多少条笔记
     *
     * @param uid
     * @return
     */
    private Long findUserCollectedNoteTotal(String uid) {
        // 先查询记录总量
        LambdaQueryWrapper<NoteCollection> lambdaQueryWrapper = Wrappers.<NoteCollection>lambdaQuery()
                .eq(NoteCollection::getUid, uid)
                .eq(NoteCollection::getStatus, CollectUnCollectNoteTypeEnum.COLLECT.getCode());
        return noteCollectionMapper.selectCount(lambdaQueryWrapper);
    }

    /**
     * 异步初始化用户收藏笔记 ZSet
     *
     * @param uid
     * @param userNoteCollectionZSetKey
     */
    private void asynInitUserNoteCollectionsZSet(String uid, String userNoteCollectionZSetKey) {
        threadPoolTaskExecutor.execute(() -> {
            // 判断用户笔记收藏 ZSET 是否存在
            Boolean hasKey = redisTemplate.hasKey(userNoteCollectionZSetKey);

            // 不存在，则重新初始化
            if (!hasKey) {
                // 查询最近收藏的 300 条笔记
                List<NoteCollection> noteCollections = selectCollectedNotes(uid);

                if (CollUtil.isNotEmpty(noteCollections)) {
                    // 保底 1 天 + 随机秒数
                    long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
                    // 构建 Lua 参数
                    Object[] lugArgs = buildNoteCollectionZSetLuaArgs(noteCollections, expireSeconds);

                    DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                    // Lua 脚本路径
                    script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_collect_zset_and_expire.lua")));
                    // 返回值类型
                    script.setResultType(Long.class);

                    // 初始化 ZSet 列表
                    redisTemplate.execute(script, Collections.singletonList(userNoteCollectionZSetKey), lugArgs);
                }
            }
        });
    }

    /**
     * 构建收藏笔记 Lua 脚本参数
     *
     * @param noteCollections
     * @param expireSeconds
     * @return
     */
    private Object[] buildNoteCollectionZSetLuaArgs(List<NoteCollection> noteCollections, long expireSeconds) {
        // 每个笔记收藏关系有 2 个参数（score 和 value），最后再跟一个过期时间
        int argsLength = noteCollections.size() * 2 + 1;
        Object[] lugArgs = new Object[argsLength];

        int i = 0;
        for (NoteCollection noteCollection : noteCollections) {
            // 点赞时间作为 score
            lugArgs[i] = DateUtils.localDateTime2Timestamp(noteCollection.getCreateTime());
            // 笔记ID 作为 ZSet value
            lugArgs[i + 1] = noteCollection.getNoteId();
            i += 2;
        }

        // 最后一个参数是 ZSet 的过期时间
        lugArgs[argsLength - 1] = expireSeconds;
        return lugArgs;
    }

    /**
     * 查询用户收藏过的笔记
     *
     * @param uid
     * @return
     */
    private List<NoteCollection> selectCollectedNotes(String uid) {
        LambdaQueryWrapper<NoteCollection> lambdaQueryWrapper = Wrappers.<NoteCollection>lambdaQuery()
                .select(NoteCollection::getNoteId, NoteCollection::getCreateTime)
                .eq(NoteCollection::getUid, uid)
                .eq(NoteCollection::getStatus, CollectUnCollectNoteTypeEnum.COLLECT.getCode())
                .orderByDesc(NoteCollection::getCreateTime)
                .last("limit 300");

        return noteCollectionMapper.selectList(lambdaQueryWrapper);
    }

    /**
     * 初始化收藏布隆过滤器
     *
     * @param uid
     * @param expireSeconds
     * @param bloomUserNoteCollectListKey
     */
    private void batchAddNoteCollect2BloomAndExpire(String uid, long expireSeconds, String bloomUserNoteCollectListKey) {
        try {
            // 查询用户是否收藏过其他笔记
            LambdaQueryWrapper<NoteCollection> lambdaQueryWrapper = Wrappers.<NoteCollection>lambdaQuery()
                    .select(NoteCollection::getNoteId)
                    .eq(NoteCollection::getUid, uid)
                    .eq(NoteCollection::getStatus, CollectUnCollectNoteTypeEnum.COLLECT.getCode());
            List<NoteCollection> noteCollections = noteCollectionMapper.selectList(lambdaQueryWrapper);

            if (CollUtil.isNotEmpty(noteCollections)) {
                DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                // Lua 脚本路径
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_batch_add_note_collect_and_expire.lua")));
                // 返回值类型
                script.setResultType(Long.class);

                // 构建 Lua 参数
                List<Object> luaArgs = Lists.newArrayList();
                // 将每个收藏的笔记 ID 传入
                noteCollections.forEach(noteCollection -> luaArgs.add(noteCollection.getNoteId()));
                // 最后一个参数是过期时间（秒）
                luaArgs.add(expireSeconds);
                redisTemplate.execute(script, Collections.singletonList(bloomUserNoteCollectListKey), luaArgs.toArray());
            }
        } catch (Exception e) {
            log.error("## 异步初始化【笔记收藏】布隆过滤器异常: ", e);
        }

    }

    /**
     * 查询用户是否收藏过笔记
     *
     * @param uid
     * @param noteId
     * @return
     */
    private Long selectNoteIsCollected(String uid, String noteId) {
        Long count;
        LambdaQueryWrapper<NoteCollection> lambdaQueryWrapper = Wrappers.<NoteCollection>lambdaQuery()
                .eq(NoteCollection::getUid, uid)
                .eq(NoteCollection::getNoteId, noteId)
                .eq(NoteCollection::getStatus, CollectUnCollectNoteTypeEnum.COLLECT.getCode())
                .last("limit 1");
        // 查找用户是否点赞过该笔记
        count = noteCollectionMapper.selectCount(lambdaQueryWrapper);
        return count;
    }

    /**
     * 校验笔记是否存在
     *
     * @param noteId
     */
    private String checkNoteIsExist(String noteId) {
        // 构建查询条件
        Query query = Query.query(Criteria.where("_id").is(noteId));
        // 筛选字段
        query.fields().include("uid");
        // 查询笔记
        CircleContext context = mongoTemplate.findOne(query, CircleContext.class);

        // 数据库不存在该笔记，提示用户
        if (Objects.isNull(context)) {
            throw new CustomException(ResponseCodeEnum.NOTE_NOT_FIND);
        }

        // 返回用户ID
        return context.getUid();
    }

    /**
     * 查询用户点赞过的笔记
     *
     * @param uid
     * @return
     */
    private List<NoteLike> selectLikedNotes(String uid) {
        LambdaQueryWrapper<NoteLike> lambdaQueryWrapper = Wrappers.<NoteLike>lambdaQuery()
                .select(NoteLike::getNoteId, NoteLike::getCreateTime)
                .eq(NoteLike::getUid, uid)
                .eq(NoteLike::getStatus, LikeUnlikeNoteTypeEnum.LIKE.getCode())
                .orderByDesc(NoteLike::getCreateTime)
                .last("limit 100");
        return noteLikeMapper.selectList(lambdaQueryWrapper);
    }

    /**
     * 异步初始化用户点赞笔记 ZSet
     *
     * @param uid
     * @param userNoteLikeZSetKey
     */
    private void asynInitUserNoteLikesZSet(String uid, String userNoteLikeZSetKey) {
        threadPoolTaskExecutor.execute(() -> {
            // 判断用户笔记点赞 ZSET 是否存在
            Boolean hasKey = redisTemplate.hasKey(userNoteLikeZSetKey);

            // 不存在，则重新初始化
            if (!hasKey) {
                // 查询最近点赞的100条笔记
                List<NoteLike> noteLikes = selectLikedNotes(uid);

                if (CollUtil.isNotEmpty(noteLikes)) {
                    // 保底 1 天 + 随机秒数
                    long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
                    // 构建 Lua 参数
                    Object[] lugArgs = buildNoteLikeZSetLuaArgs(noteLikes, expireSeconds);

                    DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                    // Lua 脚本路径
                    script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_like_zset_and_expire.lua")));
                    // 返回值类型
                    script.setResultType(Long.class);

                    // 初始化 ZSet 列表
                    redisTemplate.execute(script, Collections.singletonList(userNoteLikeZSetKey), lugArgs);
                }
            }
        });
    }

    /**
     * 构建点赞笔记 Lua 脚本参数
     *
     * @param noteLikes
     * @param expireSeconds
     * @return
     */
    private Object[] buildNoteLikeZSetLuaArgs(List<NoteLike> noteLikes, long expireSeconds) {
        // 每个笔记点赞关系有 2 个参数（score 和 value），最后再跟一个过期时间
        int argsLength = noteLikes.size() * 2 + 1;
        Object[] luaArgs = new Object[argsLength];

        int i = 0;
        for (NoteLike noteLike : noteLikes) {
            // 点赞时间作为 score
            luaArgs[i] = DateUtils.localDateTime2Timestamp(noteLike.getCreateTime());
            // 笔记ID 作为 ZSet value
            luaArgs[i + 1] = noteLike.getNoteId();
            i += 2;
        }

        // 最后一个参数是 ZSet 的过期时间
        luaArgs[argsLength - 1] = expireSeconds;
        return luaArgs;
    }

    /**
     * 查询用户是否点赞过笔记
     *
     * @param uid
     * @param noteId
     * @return
     */
    private Long selectNoteIsLiked(String uid, String noteId) {
        Long count;
        LambdaQueryWrapper<NoteLike> lambdaQueryWrapper = Wrappers.<NoteLike>lambdaQuery()
                .eq(NoteLike::getUid, uid)
                .eq(NoteLike::getNoteId, noteId)
                .eq(NoteLike::getStatus, LikeUnlikeNoteTypeEnum.LIKE.getCode())
                .last("limit 1");
        // 查找用户是否点赞过该笔记
        count = noteLikeMapper.selectCount(lambdaQueryWrapper);
        return count;
    }

    /**
     * 初始化点赞布隆过滤器
     *
     * @param uid
     * @param expireSeconds
     * @param bloomUserNoteLikeListKey
     */
    private void batchAddNoteLike2BloomAndExpire(String uid, long expireSeconds, String bloomUserNoteLikeListKey) {
        try {
            // 异步全量同步一下，并设置过期时间
            // 查找用户是否点赞过的笔记
            LambdaQueryWrapper<NoteLike> lambdaQueryWrapper = Wrappers.<NoteLike>lambdaQuery()
                    .select(NoteLike::getNoteId)
                    .eq(NoteLike::getUid, uid)
                    .eq(NoteLike::getStatus, LikeUnlikeNoteTypeEnum.LIKE.getCode());
            List<NoteLike> noteLikes = noteLikeMapper.selectList(lambdaQueryWrapper);

            if (CollUtil.isNotEmpty(noteLikes)) {
                DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                // Lua 脚本路径
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_batch_add_note_like_and_expire.lua")));
                // 返回值类型
                script.setResultType(Long.class);

                // 构建 Lua 参数
                List<Object> luaArgs = Lists.newArrayList();
                // 将每个点赞的笔记 ID 传入
                noteLikes.forEach(noteLike -> luaArgs.add(noteLike.getNoteId()));
                // 最后一个参数是过期时间（秒）
                luaArgs.add(expireSeconds);
                redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), luaArgs.toArray());
            }
        } catch (Exception e) {
            log.error("## 异步初始化【笔记点赞】布隆过滤器异常: ", e);
        }
    }
}
